/*
 * Copyright (c) 2009, Russell Weir
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdlib.h>
#include <string.h>
#include "hxvm_lua.h"
#include "hashtool.h"

luaL_reg luaLibs[] = {
	{ "base",	luaopen_base },
	{ "table",	luaopen_table },
	{ "io",		luaopen_io },
	{ "os",		luaopen_os },
	{ "string",	luaopen_string },
	{ "math",	luaopen_math },
	{ "debug",	luaopen_debug },
	{ "package",luaopen_package },
	{ NULL,		NULL }
};

typedef struct IterData {
	lua_State *L;
	int ok;
	value object;
} IterData;

DEFINE_KIND( k_lua_vm );

// forward declarations
static int vmlua_push_value(lua_State *L, value v);
static void vmlua_push_fields_rec( value data, field fId, void *stat );
static value vmlua_create_value(lua_State *L, int idx, unsigned int pop=1);
// common methods
#include "hxvm.cpp"

/**
* Converts a relative stack position (<0) to an absolute position.
* If the provided index is already absolute, then no change
* occurs
*/
#define ABS_POS(L,idx) if(idx < 0) idx = lua_gettop(L) + idx + 1; if(idx < 0) idx = 0;

/**
* Returns the VM_Descriptor from the neko handle
**/
static VM_Descriptor* vmlua_get_descriptor( value vm )
{
	if ( !val_is_kind( vm, k_lua_vm ) )
		return NULL;
	VM_Descriptor *vd = VM( vm );
	if(!vd->lua_state) {
		fprintf(stderr, "Bad lua_state\n");
		return NULL;
	}
	return vd;
}

/**
* Convenience method for setting the last_error off the
* stack, and returning a neko String value for it.
*
* @return neko_error always
**/
static value vmlua_stack_error(VM_Descriptor *vd, const char *defaultMsg=NULL)
{
	if(!vd) neko_error();
	lua_State *L = vd->lua_state;
	if( vd->type == VM_LUA ) {
		if(lua_gettop(L) > 0 && lua_isstring(L, -1))
			setError(vd, lua_tostring(vd->lua_state, -1));
		else if(defaultMsg != NULL)
			setError(vd, defaultMsg);
		else
			setError(vd, "Unknown error.");
	}
	lua_settop(L,0);
	neko_error();
}

static value vmlua_char_error(VM_Descriptor *vd, const char *err)
{
	setError(vd, err);
	neko_error();
}

/**
* Free the Lua vm object
**/
void vmlua_destroy( value vm )
{
	if ( !val_is_kind( vm, k_lua_vm ) )
		return;
	VM_Descriptor *vd = VM( vm );
	val_gc( vm, NULL );

	if(!vd) return;
	if(vd->lua_state) {
		lua_close( vd->lua_state );
		vd->lua_state = NULL;
	}

	clearError(vd);
	free( vd );
}

/**
* Checks if the table at a specific stack position can be considered an
* 'array' type. This checks that all indexes are of 'number' type, and
* returns true if so.
**/
static int vmlua_is_array(lua_State *L, int idx)
{
	ABS_POS(L,idx);
	int rv = 1;
	// table is in the stack at index 'idx'
	lua_pushnil(L);  // first key
	while (lua_next(L, idx) != 0) {
		// 'key' at -2 and 'value' at -1
		if( lua_type(L, -2) != LUA_TNUMBER ) {
			rv = 0;
			lua_pop(L, 2); // pop key and value
			break;
		}
		// pop 'value', keep 'key' for next iteration
		lua_pop(L, 1);
	}
	return rv;
}

/**
* Converts the table at index 'idx' to a neko array
*/
static value vmlua_to_array(lua_State *L, int idx)
{
	ABS_POS(L,idx);
	// get size of table
	size_t len = luaL_getn(L, idx);
	// alloc neko array
	value rv = alloc_array( len );
	for (int i=1; i<=len; i++) {
		lua_rawgeti(L, idx, i);
		val_array_ptr(rv)[i-1] = vmlua_create_value(L,-1);
	}
	return rv;
}

/**
* Converts the table at index 'idx' to a neko hash
*/
static value vmlua_to_hash(lua_State *L, int idx)
{
	value key, val;
	ABS_POS(L,idx);
	vhash *h = hash_new();
	// table is in the stack at index 'idx'
	lua_pushnil(L);  // first key
	while (lua_next(L, idx) != 0) {
		// key at -2 and value at -1
		val = vmlua_create_value(L, -1); // pop value
		if(val_is_float(val))
			val = alloc_int((int)val_float(val));
		key = vmlua_create_value(L, -1, 0); // don't pop key
		hash_set(h, key, val);
	}

	return hash_to_value(h);
}

/**
* Recursive callback for neko objects
*
* @param data Object field value
* @param fId Field id
* @param stat IterData pointer
**/
static void vmlua_push_fields_rec( value data, field fId, void *stat )
{
	IterData* idata = (IterData *) stat;

	if(!idata->ok)
		return;

	value key = val_field_name( fId );
	if(val_is_string(key)) {
		lua_pushstring(idata->L, val_string(key)); // key
		if(!vmlua_push_value(idata->L, data)) {
			idata->ok = 0;
			lua_pushnil(idata->L);
		}
		lua_settable(idata->L, -3);
	}
	else {
		fprintf(stderr, "vmlua_push_fields_rec: Internal error #%d. Please report.\n", val_type(key));
		idata->ok = 0;
	}
}

/**
* Translate a neko value, and push it onto the Lua stack.
*
* @param L Lua state
* @param v Neko value
* @return number of items pushed, 0 on failure.
**/
static int vmlua_push_value(lua_State *L, value v)
{
	int len, i, pos;
	IterData idata;

	switch( val_type(v) ) {
	case VAL_NULL:
		lua_pushnil(L);
		break;
	case VAL_INT:
		lua_pushinteger(L, val_int(v));
		break;
	case VAL_FLOAT:
		lua_pushnumber(L, val_float(v));
		break;
	case VAL_BOOL:
		lua_pushboolean(L, val_bool(v));
		break;
	case VAL_ARRAY:
		len = val_array_size(v);
		lua_newtable(L); // create table on the stack
		for( i=0; i<len; i++) {
			lua_pushinteger(L, i+1); // key
			vmlua_push_value(L, val_array_ptr(v)[i]);
			lua_settable(L, -3);
		}
		break;
	case VAL_FUNCTION:
		lua_pushlightuserdata(L, ((vfunction*)(v))->addr);
		break;
	case VAL_STRING:
		lua_pushlstring(L, val_string(v), val_strlen(v));
		break;
	case VAL_OBJECT:
		lua_newtable(L); // create table on the stack
		idata.L = L;
		idata.ok = 1;
		idata.object = v;
		val_iter_fields(v, vmlua_push_fields_rec, &idata);
		break;
	case VAL_ABSTRACT:
		if( val_is_kind(v, k_int32) ) {
			lua_pushinteger(L, val_int32(v));
		}
		else if( val_is_kind(v,k_hash) ) {
			vhash *h = val_hdata(v);
			lua_newtable(L); // create table on the stack
			for(i=0; i<h->ncells; i++) {
				hcell *c = h->cells[i];
				while( c != NULL ) {
					vmlua_push_value(L, c->key);
					vmlua_push_value(L, c->val);
					lua_settable(L, -3);
					c = c->next;
				}
			}
		}
		else if(val_is_kind(v, k_pointer)) {
			VM_Pointer *p = POINTER(v);
			lua_pushlightuserdata (L, p->data);
		}
		else
			return 0;
		break;
	default:
		fprintf(stderr, "Unhandled neko type %d\n", val_type(v));
		return 0;
		break;
	}
	return 1;
}

/**
* Returns the top lua stack item as a neko value
*
* @param L Lua state
* @param idx Relative or aqbsolute stack index.
* @param pop Set to True to remove item from stack
*/
static value vmlua_create_value(lua_State *L, int idx, unsigned int pop)
{
	size_t len;
	const char *sc;
	value rv = val_null;
	VM_Pointer *ptr;
	int i;
	void *u;

	ABS_POS(L,idx);

	switch (lua_type(L, idx)) {		// get the type of the returned value
	case LUA_TNONE:
		rv = val_null;
		pop = 0;
		break;
	case LUA_TNIL:
		rv = val_null;
		break;
	case LUA_TNUMBER:
		rv = alloc_float(lua_tonumber(L,idx));
		break;
	case LUA_TBOOLEAN:
		rv = alloc_bool(lua_toboolean(L,idx));
		break;
	case LUA_TSTRING:
		sc = lua_tolstring(L,idx,&len);
		rv = copy_string(sc,len);
		break;
	case LUA_TTABLE:
		if(vmlua_is_array(L,idx))
			rv = vmlua_to_array(L, idx);
		else
			rv = vmlua_to_hash(L, idx);
		break;
	case LUA_TFUNCTION:
		ptr = (VM_Pointer *)malloc(sizeof(VM_Pointer));
		ptr->type = PTR_FUNCTION;
		ptr->data = NULL;
		rv = alloc_abstract( k_pointer, ptr );
		val_gc( rv, hxvm_destroy_pointer );
		u = lua_touserdata(L,idx);
		ptr->data = (u==NULL) ? NULL : *((void**)u);
		break;
	case LUA_TTHREAD:
		fprintf(stderr, "LUA_TTHREAD Lua type not handled\n");
		break;
	case LUA_TLIGHTUSERDATA:
		ptr = (VM_Pointer *)malloc(sizeof(VM_Pointer));
		ptr->type = PTR_DATA;
		ptr->data = lua_touserdata(L,idx);
		rv = alloc_abstract( k_pointer, ptr );
		val_gc( rv, hxvm_destroy_pointer );
		break;
	case LUA_TUSERDATA:
		fprintf(stderr, "LUA_TUSERDATA Lua type not handled\n");
		break;
	default:
		fprintf(stderr, "Unexpected Lua type #%d\n", lua_type(L, idx));
	}

	if(pop)
		lua_remove(L, idx);

	return rv;
}

/**
* Do a Lua call. Correct procedure is to lua_settop(0), push function name,
* push arguments, then call this method. The lua stack will be reset to 0
* after a call to this function.
*
* @param vd Valid vm pointer
* @param argc Number of args that have been pushed onto stack
* @param multiRet Multiple return value mode on (true) or off.
* @return value Array of return values (LUA_MULTRET), single value, or neko_error (NULL)
**/
static value vmlua_do_call(VM_Descriptor *vd, int argc, int multiRet)
{
	if(!vd || !vd->lua_state)
		neko_error();
	lua_State *L = vd->lua_state;
	value rv = NULL;
	int nresults = multiRet ? LUA_MULTRET : 1;
	int i = lua_pcall(L, argc, nresults, 0);

	// return neko_error on error
	if (i != 0)
		return vmlua_stack_error(vd, "Function call error.");

	// single return mode
	if(nresults == 1) {
		rv = vmlua_create_value(L, 1);
		lua_settop(L,0);
		return rv;
	}

	// multiReturnMode
	int n = lua_gettop(L); // get #of results
	if(n > 0) {
		rv = alloc_array( n );
		for (i=0; i<n; i++) {
			// don't bother popping value, stack will be cleared
			val_array_ptr(rv)[ i ] = vmlua_create_value(L, i+1, 0);
		}
	}
	else {
		rv = alloc_array( 0 );
	}
	lua_settop(L,0);
	return rv;
}


////////////////////////////////////////////////////////////
////                    LUA API                         ////
////////////////////////////////////////////////////////////
/**
* Startup
**/
value vmlua_initialize()
{
	return val_true;
}
DEFINE_PRIM( vmlua_initialize, 0 );

/**
* Returns the last error as a string. If there is no
* error, null is returned.
*
* @return String error message or null
**/
value vmlua_get_error(value vm)
{
	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd)
		return alloc_string("VM not allocated or pointer invalid.");

	if(vd->last_error == NULL)
		return alloc_string("");
	value rv = alloc_string((const char *) vd->last_error);
	return rv;
}
DEFINE_PRIM( vmlua_get_error, 1 );

/**
* Create a new Lua vm
*
* @param loadStdLibs Set to true to load all standard libraries
**/
value vmlua_create(value loadStdLibs)
{
	value rv;

	VM_Descriptor *vd = (VM_Descriptor *)malloc(sizeof(VM_Descriptor));
	if(!vd) neko_error();
	vd->type == VM_UNKNOWN;
	vd->multiReturnMode = 0;
	vd->last_error = NULL;

	// init Lua
	//printf("Creating lua vm\n");
	vd->lua_state = lua_open();
	if(!vd->lua_state)
		neko_error();

	vd->type = VM_LUA;
	if( vd->lua_state == NULL )
		goto fatal;

	// load standard libs
	const luaL_reg *lib;
	if(val_bool(loadStdLibs)) {
		for (lib = luaLibs; lib->func != NULL; lib++) {
			lua_pushcfunction(vd->lua_state, lib->func);
			lua_pushstring(vd->lua_state, lib->name);
			lua_call(vd->lua_state, 1, 0);
		}
	}

	rv = alloc_abstract( k_lua_vm, vd );
	val_gc( rv, vmlua_destroy );

	return rv;
fatal:
	fprintf(stderr, "vm create failed\n");
	free(vd);
	return val_null;
}
DEFINE_PRIM( vmlua_create, 1 );

/**
* Returns the runtime version of the VM as a string
*
* @return Full vm version string, or null
**/
value vmlua_getversion(value vm)
{
	VM_Descriptor *vd = vmlua_get_descriptor(vm);
	if(!vd) neko_error();
	lua_State *L = vd->lua_state;

	if(L) {
		lua_getfield(L, LUA_GLOBALSINDEX, "_VERSION");
		return vmlua_create_value(L, -1);
	}

	return val_null;
}
DEFINE_PRIM( vmlua_getversion, 1 );

/**
* Open a standard library
*
* @param vm
* @param name Library name to open
* @return Bool true on success
**/
value vmlua_openstdlib(value vm, value name)
{
	VM_Descriptor *vd = vmlua_get_descriptor(vm);
	if(!vd) neko_error();
	val_check(name, string);

	lua_State *L = vd->lua_state;
	const char *libName = val_string(name);
	lua_CFunction func = NULL;

	if(!strcmp(libName, "base") || !strcmp(libName, "coroutine"))
		func = luaopen_base;
	else if(!strcmp(libName, "table"))
		func = luaopen_table;
	else if(!strcmp(libName, "io"))
		func = luaopen_io;
	else if(!strcmp(libName, "os"))
		func = luaopen_os;
	else if(!strcmp(libName, "string"))
		func = luaopen_string;
	else if(!strcmp(libName, "math"))
		func = luaopen_math;
	else if(!strcmp(libName, "debug"))
		func = luaopen_debug;
	else if(!strcmp(libName, "package"))
		func = luaopen_package;
	if(func) {
		lua_pushcfunction(L, func);
		lua_pushstring(vd->lua_state, libName);
		lua_call(vd->lua_state, 1, 0);
	}
	else
		return val_false;

	return val_true;
}
DEFINE_PRIM( vmlua_openstdlib, 2 );

/**
* Set the vm default paths
*
* @param vm
* @param pathInfo String path formatted properly for the VM
**/
value vmlua_set_path(value vm, value pathInfo)
{
	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd) neko_error();
	val_check( pathInfo, string );

	const char* path = val_string( pathInfo );

	lua_pushstring(vd->lua_state, path);
	lua_setglobal(vd->lua_state, "LUA_PATH");

	lua_getglobal(vd->lua_state, "package");
	if (lua_istable(vd->lua_state, -1)) {
		lua_pushstring(vd->lua_state, path);
		lua_setfield(vd->lua_state, -2, "path");
	}
	lua_settop(vd->lua_state, 0);


	return val_null;
}
DEFINE_PRIM( vmlua_set_path, 2 );

/**
* Loads a script from a file path
*
* @param vm vm pointer
* @param filename String file path
* @return Bool true on success, false on any failure
**/
value vmlua_load_file(value vm, value filename)
{
	int res = 0;

	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd) neko_error();

	if( val_is_null(filename) ) {
		setError(vd, "Filename is null.");
		return val_false;
	}
	val_check( filename, string );
	if(val_strlen(filename) == 0) {
		setError(vd, "Filename is empty.");
		return val_false;
	}

	clearError(vd);

	res = luaL_loadfile(vd->lua_state, val_string( filename ));
	if(res != 0) {
		vmlua_stack_error( vd, "Unable to load file." );
		return val_false;
	}

	return val_true;
}
DEFINE_PRIM( vmlua_load_file, 2 );

/**
* Exec a block of code contained in a byte buffer
*
* @param vm
* @param filename Lua file to run
* @return val_null or return value based on vd->multiReturnMode
**/
value vmlua_exec_file(value vm, value filename)
{
	value rv = vmlua_load_file(vm, filename);
	if(!rv || !val_bool(rv)) neko_error();

	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd) neko_error();
	// parse script

	return vmlua_do_call(vd, 0, vd->multiReturnMode);
}
DEFINE_PRIM( vmlua_exec_file, 2 );

/**
* Loads a script from a byte buffer
*
* @param vm vm pointer
* @param code string containing code to run
* @param chunkName optional name for code chunk.
* @return Bool true on success
**/
value vmlua_load_bytes(value vm, value code, value chunkName) {
	const char* chunk;
	int res = 0;

	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd) neko_error();
	if(val_is_null(code)) {
		setError(vd, "Null code.");
		return val_false;
	}
	val_check( code, string );
	if(val_strlen(code) == 0) {
		setError(vd, "Empty code.");
		return val_false;
	}
	if(val_is_null(chunkName)) {
		chunk = "bytes";
	}
	else {
		val_check(chunkName, string);
		chunk = val_string(chunkName);
	}

	clearError(vd);

	res = luaL_loadbuffer (vd->lua_state, val_string(code), val_strlen(code), chunk);
	if(res != 0) {
		vmlua_stack_error( vd, "Unable to load code from buffer." );
		return val_false;
	}
	return val_true;
}
DEFINE_PRIM( vmlua_load_bytes, 3 );

/**
* Exec a block of code contained in a byte buffer
*
* @param vm
* @param code Haxe bytes
* @param chunkName Lua chunkName or null
* @return val_null or return value based on vd->multiReturnMode
**/
value vmlua_exec(value vm, value code, value chunkName) {
	value rv = vmlua_load_bytes(vm, code, chunkName);
	if(!rv || !val_bool(rv)) neko_error();

	VM_Descriptor *vd = vmlua_get_descriptor( vm );
	if(!vd) neko_error();

	return vmlua_do_call(vd, 0, vd->multiReturnMode);
}
DEFINE_PRIM( vmlua_exec, 3 );

/**
* Turn on or off multiple return values mode.
*
* @param vm
* @param enabled Bool value, true for on.
**/
value vmlua_setmultireturnmode(value vm,value enabled)
{
	VM_Descriptor *vd = vmlua_get_descriptor(vm);
	if( !vd ) neko_error();
	val_check(enabled, bool);

	if(val_bool(enabled))
		vd->multiReturnMode = 1;
	else
		vd->multiReturnMode = 0;
	return enabled;
}
DEFINE_PRIM( vmlua_setmultireturnmode, 2 );

/**
* Executes function in virtual machine. Return value is
* the function return value, or false with vm error set.
* If false is returned, the hxvm_get_error function should
* be called to see if an error exists.
*
* @param vm VM handle
* @param functionName String name of function to call
* @param args Array of arguments to function
**/
value vmlua_call_function(value vm, value functionName, value inArgs)
{
	VM_Descriptor *vd = vmlua_get_descriptor(vm);
	if(!vd) neko_error();
	clearError(vd);
	val_check( functionName, string );
	val_check( inArgs, array );

	int i, n, stackStart;
	lua_State *L = vd->lua_state;

	lua_settop(L, 0);
	stackStart = lua_gettop(L);
	// push function name
	lua_getfield(L, LUA_GLOBALSINDEX, val_string(functionName));

	// push args
	n = val_array_size(inArgs);
	value* args = val_array_ptr(inArgs);
	for(i=0; i<n; i++) {
		if(!vmlua_push_value(L, args[i])) {
			lua_settop(L,0);
			clearError( vd );
			vd->last_error = (char *)malloc(200 + val_strlen(functionName));
			sprintf(vd->last_error, "lua call : %s : invalid type for argument %d", val_string(functionName), i+1);
			neko_error();
		}
	}
	return vmlua_do_call(vd, n, vd->multiReturnMode);
}
DEFINE_PRIM( vmlua_call_function, 3 );

/**
* Creates a native date object. Lua uses plain objects, so this is
* not a necessary call.
*
* @param vm
* @param oDate A VmDateObject.toObject() value
* @return An object, an instance handle or null
*/
value vmlua_makenativedate(value vm, value oDate)
{
	VM_Descriptor *vd = vmlua_get_descriptor(vm);
	if( !vd ) neko_error();

	return oDate;
}
DEFINE_PRIM( vmlua_makenativedate, 2 );
